<?php
/**
 * Nweb Framework
 *
 * This script is protected by copyright. It's use, copying, modification
 * and distribution without written consent of the author is prohibited.
 *
 * @category    source
 * @package     pl.nweb.framework.http
 * @subpackage  client.adapter
 * @author      Krzysztof Kardasz <krzysztof.kardasz@gmail.com>
 * @copyright   Copyright (c) 2011 Krzysztof Kardasz
 * @license     http://www.gnu.org/licenses/lgpl-3.0.txt  GNU Lesser General Public
 * @version     3.0 $Id: curl.php 452 2011-12-07 12:04:19Z krzysztof.kardasz $
 * @link        http://code.google.com/p/nweb-framework/
 * @link        http://framework.nweb.pl
 */

namespace pl\nweb\framework\http\client\adapter;

/**
 * Adapter Curl
 *
 * @category    source
 * @package     pl.nweb.framework.http
 * @subpackage  client.adapter
 * @author      Krzysztof Kardasz <krzysztof.kardasz@gmail.com>
 * @copyright   Copyright (c) 2011 Krzysztof Kardasz
 * @version     3.0 $Id: curl.php 452 2011-12-07 12:04:19Z krzysztof.kardasz $
 */
class Curl extends \pl\nweb\framework\http\client\Adapter
{
    /**
     * Sesja
     *
     * @var resource|null
     */
    private $_curl = null;

    /**
     * Parametry
     *
     * @var array
     */
    protected $_options = array();

    /**
     * Odpowiedź
     *
     * @var null|string
     */
    protected $_response = null;

    /**
     * Mapowanie metod
     *
     * @var array
     */
    private $_methodMap = array(
        self::METHOD_GET     => array(CURLOPT_HTTPGET, true),
        self::METHOD_POST    => array(CURLOPT_POST, true),
        self::METHOD_PUT     => array(CURLOPT_PUT, true),
        self::METHOD_DELETE  => array(CURLOPT_CUSTOMREQUEST, 'DELETE'),
        self::METHOD_TRACE   => array(CURLOPT_CUSTOMREQUEST, 'TRACE'),
        self::METHOD_HEAD    => array(CURLOPT_CUSTOMREQUEST, 'HEAD'),
        self::METHOD_OPTIONS => array(CURLOPT_CUSTOMREQUEST, 'OPTIONS')
    );

    /**
     * Opcje, które nie mogą być nadpisywane
     *
     * @var array
     */
    private $_invalidOverwritableOptions = array(
        CURLOPT_HTTPGET,
        CURLOPT_POST,
        CURLOPT_PUT,
        CURLOPT_CUSTOMREQUEST,
        CURLOPT_HEADER,
        CURLOPT_RETURNTRANSFER,
        CURLOPT_HTTPHEADER,
        CURLOPT_POSTFIELDS,
        CURLOPT_INFILE,
        CURLOPT_INFILESIZE,
        CURLOPT_PORT,
        CURLOPT_MAXREDIRS,
        CURLOPT_CONNECTTIMEOUT,
        CURL_HTTP_VERSION_1_1,
        CURL_HTTP_VERSION_1_0
    );

    /**
     * Konfiguracja
     */
    const CONFIG_TIMEOUT       = 'timeout';
    const CONFIG_MAX_REDIRECTS = 'max_redirects';
    const CONFIG_SSLCERT       = 'sslcert';
    const CONFIG_SSLPASSPHRASE = 'sslpassphrase';

    /**
     * Konstruktor
     *
     * @return void
     * @throws \pl\nweb\framework\http\client\adapter\Exception
     */
    public function __construct()
    {
        if (!extension_loaded('curl')) {
            throw new Exception('cURL extension is not available.', 1321, $this);
        }
    }

    /**
     * Definiuje parametr(y)
     *
     * @param  array|string $key klucz/tablica wartości
     * @param  mixed $value wartość dla klucza
     * @return void
     */
    public function setOption($key, $value = null)
    {
        if (is_array($key)) {
            $this->_options = $key;
        } else {
            $this->_options[$key] = $value;
        }
    }

    /**
     * Zwraca parametr(y)
     *
     * @param  null|string $key klucz
     * @param  null|mixed $default domyślna wartość
     * @return void
     */
    public function getOption($key = null, $default = null)
    {
        if (null === $key) {
            return $this->_options;
        }
        if (isset($this->_options[$key])) {
            return $this->_options[$key];
        }
        return $default;
    }

    /**
     * Usuwa parametr
     *
     * @param  string $key klucz
     * @return void
     */
    public function removeOption($key)
    {
        if (isset($this->_option[$key])) {
            unset($this->_option[$key]);
        }
    }

    /**
     * Połączenie
     *
     * @return void
     * @throws \pl\nweb\framework\http\client\adapter\Exception
     */
    public function connect()
    {
        if (null !== $this->_curl) {
            $this->close();
        }
        $uri = parse_url($this->_uri);

        $host = $uri['host'];
        $secure = $uri['scheme'] === 'https';
        $port = (isset($uri['port'])) ? (int)$uri['port'] : 80;

        // Inicjacja
        $this->_curl = curl_init();

        if ($port !== 80) {
            curl_setopt($this->_curl, CURLOPT_PORT, $port);
        }

        // Czas oczekiwania
        curl_setopt($this->_curl, CURLOPT_CONNECTTIMEOUT, $this->getConfig(
            self::CONFIG_TIMEOUT,
            $this->getOption(CURLOPT_CONNECTTIMEOUT, 10)
        ));

        // Maksymalna ilość przekierowań
        curl_setopt($this->_curl, CURLOPT_MAXREDIRS, $this->getConfig(
            self::CONFIG_MAX_REDIRECTS,
            $this->getOption(CURLOPT_MAXREDIRS, 2)
        ));

        if (!$this->_curl) {
            $this->close();
            throw new Exception('Unable to Connect to ' . $host . ':' . $port, 1322, $this);
        }

        if ($secure) {
            if ($sslcert = $this->getConfig(
                self::CONFIG_SSLCERT,
                $this->getOption(CURLOPT_SSLCERT)
            )) {
                curl_setopt($this->_curl, CURLOPT_SSLCERT, $sslcert);
            }
            if ($sslpassphrase = $this->getConfig(
                self::CONFIG_SSLPASSPHRASE,
                $this->getOption(CURLOPT_SSLCERTPASSWD)
            )) {
                curl_setopt($this->_curl, CURLOPT_SSLCERTPASSWD, $sslpassphrase);
            }
        }
    }

    /**
     * Zamyka połączenie z serwerem
     *
     * @return void
     */
    public function close()
    {
        if (null !== $this->_curl) {
            curl_close($this->_curl);
            $this->_curl = null;
        }
    }

    /**
     * Wysyła żądanie
     *
     * @return \pl\nweb\framework\http\Request
     * @throws \pl\nweb\framework\http\client\adapter\Exception
     */
    protected function _write()
    {
        if (null === $this->_curl) {
            $this->connect();
        }

        $url = $this->_uri;
        if(count($this->_get) > 0) {
            $url .= '?' . http_build_query($this->_get);
        }

        // URL
        curl_setopt($this->_curl, CURLOPT_URL, $url);

        if ($this->_method == self::METHOD_PUT) {
            if (is_resource($this->_body)) {
                $this->setOption(CURLOPT_INFILE, $this->_body);
            }
            if ($this->getOption(CURLOPT_INFILE)) {
                foreach ($this->_headers as $k => $header) {
                    if (preg_match('/Content-Length:\s*(\d+)/i', $header, $m)) {
                        if (is_resource($this->_body)) {
                            $this->_config['curloptions'][CURLOPT_INFILESIZE] = (int)$m[1];
                        }
                        unset($this->_headers[$k]);
                    }
                }
                if (!$this->getOption(CURLOPT_INFILESIZE)) {
                    throw new Exception('Cannot set a file-handle for cURL option CURLOPT_INFILE without also setting its size in CURLOPT_INFILESIZE.', 1323, $this);
                }
                if (is_resource($this->_body)) {
                    $this->_body = '';
                }
            } else {
                $curlMethod = CURLOPT_CUSTOMREQUEST;
                $curlValue = 'PUT';
            }
        } else {
            if (null == $this->_method || !isset($this->_methodMap[$this->_method])) {
                throw new Exception('Method "' . $this->_method . '" is not supported', 1324, $this);
            }

            $curlMethod = $this->_methodMap[$this->_method][0];
            $curlValue  = $this->_methodMap[$this->_method][1];
        }

        if (is_resource($this->_body) && $curlMethod != CURLOPT_PUT) {
            throw new \Exception('Streaming requests are allowed only with PUT');
        }

        // Wersja protokołu
        $curlHttp = ($this->_httpVersion == self::HTTP_1_1) ? CURL_HTTP_VERSION_1_1 : CURL_HTTP_VERSION_1_0;

        // Konfiguracja wersji i metody
        curl_setopt($this->_curl, $curlHttp, true);
        curl_setopt($this->_curl, $curlMethod, $curlValue);

        // Zwracanie nagłówków
        curl_setopt($this->_curl, CURLOPT_HEADER, true);

        // Zwracanie odpowiedzi
        curl_setopt($this->_curl, CURLOPT_RETURNTRANSFER, true);

        // Dodatkowe nagłówki
        $this->_headers['Accept'] = '';
        curl_setopt($this->_curl, CURLOPT_HTTPHEADER, $this->_headers);

        if ($curlMethod == CURLOPT_PUT) {
            curl_setopt($this->_curl, CURLOPT_INFILE, $this->getOption(CURLOPT_INFILE));
            curl_setopt($this->_curl, CURLOPT_INFILESIZE, $this->getOption(CURLOPT_INFILESIZE));
            $this->removeOption(CURLOPT_INFILE);
            $this->removeOption(CURLOPT_INFILESIZE);
        } else if(!empty($this->_body) && in_array($this->_method, array(
            self::METHOD_POST, self::METHOD_PUT, self::METHOD_DELETE
        ))) {
            curl_setopt($this->_curl, CURLOPT_POSTFIELDS, $this->_body);
        }

        foreach ($this->getOption() as $k => $v) {
            if (!in_array($k, $this->_invalidOverwritableOptions)) {
                if (curl_setopt($this->_curl, $k, $v) == false) {
                    throw new Exception(sprintf('Unknown or erroreous cURL option "%s" set', $k), 1325, $this);
                }
            }
        }

        // Wysłanie żądania
        $this->_response = curl_exec($this->_curl);

        if (empty($this->_response)) {
            throw new Exception("Error in cURL request: " . curl_error($this->_curl), 1326, $this);
        }

        $requestHeaders    = array();
        $requestRawHeaders = curl_getinfo($this->_curl, CURLINFO_HEADER_OUT);


        $next = strpos($requestRawHeaders, "\n");
        $head = explode(' ', substr($requestRawHeaders, 0, $next));
        $next++;
        while(($current = strpos($requestRawHeaders, "\n", $next)) !== false) {
            $line   = trim(substr($requestRawHeaders, $next, ($current-$next)));
            $next   = $current+1;
            if($line == '') {
                break;
            }
            $header = explode(':', $line);
            if(isset($header[1])) {
                if(isset($requestHeaders[$header[0]])) {
                    if(!is_array($requestHeaders[$header[0]])) {
                        $requestHeaders[$header[0]] = array($requestHeaders[$header[0]]);
                    }
                    $requestHeaders[$header[0]][] = trim($header[1]);
                } else {
                    $requestHeaders[$header[0]]= $header[1];
                }
            }
        }

        return new \pl\nweb\framework\http\Request($head[0], $head[1], $head[2], $requestHeaders, $this->_body);
    }

    /**
     * Odczytuje odpowiedź
     *
     * @return \pl\nweb\framework\http\Response
     * @throws \pl\nweb\framework\http\client\adapter\Exception
     */
    protected function _read()
    {
        if(null === $this->_response) {
            throw new Exception ('Request is not sent', 1327, $this);
        }
        $next = strpos($this->_response, "\n");
        $http = explode(' ', trim(substr($this->_response, 0, $next)));

        // Wersja i kod odpowiedzi
        $httpVersion = $http[0];
        $httpCode    = $http[1];
        $httpDesc    = $http[2];

        $next++;

        $headers = array();

        // Magłówki
        while(($current = strpos($this->_response, "\n", $next)) !== false) {
            $line   = trim(substr($this->_response, $next, ($current-$next)));
            $next   = $current+1;

            if($line == '') {
                break;
            }
            $header = explode(':', $line);
            if(isset($headers[$header[0]])) {
                if(!is_array($headers[$header[0]])) {
                    $headers[$header[0]] = array($headers[$header[0]]);
                }
                $headers[$header[0]][] = trim($header[1]);
            } else {
                $headers[$header[0]]= $header[1];
            }
        }

        // Treść
        $body = substr(trim($this->_response), $next);

        $this->_response = null;

        return new \pl\nweb\framework\http\Response($httpVersion, $httpCode, $headers, $body);
    }
}